home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / test / templatetest.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  20.9 KB  |  460 lines

  1. import unittest
  2. import resources
  3. import os
  4. import re
  5. import time
  6. import copy
  7. import feedparser
  8. import feed
  9. import item
  10. import app
  11. import maps
  12.  
  13. from template import *
  14. import templateoptimize
  15. from time import time
  16. import database
  17. import gettext
  18. import compiled_templates
  19.  
  20. from test.framework import DemocracyTestCase
  21.  
  22. # FIXME: Add tests for DOM Handles without changeItems or deprecate
  23. #        the old changeItem API
  24.  
  25. ranOnUnload = 0
  26.  
  27. HTMLPattern = re.compile("^.*<body.*?>(.*)</body\s*>", re.S)
  28.  
  29. class HTMLObject(database.DDBObject):
  30.     def __init__(self,html):
  31.         self.html = html
  32.         database.DDBObject.__init__(self)
  33.  
  34. class DOMTracker:
  35.     def __init__(self):
  36.         self.callList = []
  37.     def addItemAtEnd(self, xml, id):
  38.         self.callList.append({'name':'addItemAtEnd','xml':xml,'id':id})
  39.     def addItemBefore(self, xml, id):
  40.         self.callList.append({'name':'addItemBefore','xml':xml,'id':id})
  41.     def removeItem(self, id):
  42.         self.callList.append({'name':'removeItem','id':id})
  43.     def removeItems(self, ids):
  44.         self.callList.append({'name':'removeItems','ids':ids})
  45.     def changeItem(self, id, xml, changeHint):
  46.         self.callList.append({'name':'changeItem','xml':xml,'id':id,
  47.             'changeHint': changeHint })
  48.     def changeItems(self, pairs):
  49.         self.callList.append({'name':'changeItems','pairs':pairs})
  50.     def hideItem(self, id):
  51.         self.callList.append({'name':'hideItem','id':id})
  52.     def showItem(self, id):
  53.         self.callList.append({'name':'showItem','id':id})
  54.  
  55. class ChangeDelayedDOMTracker(DOMTracker):
  56.     def changeItem(self, id, xml):
  57.         time.sleep(0.1)
  58.         self.callList.append({'name':'changeItem','xml':xml,'id':id})
  59.  
  60. class SimpleTest(DemocracyTestCase):
  61.     def setUp(self):
  62.         handle = file(resources.path("templates/unittest/simple"),"r")
  63.         self.text = handle.read()
  64.         handle.close()
  65.         self.text = HTMLPattern.match(self.text).group(1)
  66.     def test(self):
  67.         (tch, handle) = fillTemplate("unittest/simple",DOMTracker(),'gtk-x11-MozillaBrowser','platform')
  68.         text = tch.read()
  69.         text = HTMLPattern.match(text).group(1)
  70.         self.assertEqual(text,self.text)
  71.     def testExecuteTemplate(self):
  72.         (tch, handle) = fillTemplate("unittest/execute-template",DOMTracker(),'gtk-x11-MozillaBrowser','platform')
  73.         text = tch.read()
  74.         text = HTMLPattern.match(text).group(1)
  75.         testRE = re.compile(r'^\s+<span>foo</span>\s+<span>BAR</span>\s+$')
  76.         self.assert_(testRE.match(text))
  77.  
  78. class TranslationTest(DemocracyTestCase):
  79.     def setUp(self):
  80.         handle = file(resources.path("testdata/translation-result"),"r")
  81.         self.text = handle.read()
  82.         handle.close()
  83.         self.text = HTMLPattern.match(self.text).group(1)
  84.         self.oldgettext = gettext.gettext
  85.     def tearDown(self):
  86.         compiled_templates.unittest.translationtest._ = self.oldgettext
  87.         DemocracyTestCase.tearDown(self)
  88.     def test(self):
  89.         compiled_templates.unittest.translationtest._ = lambda x : '!%s!' % x
  90.         (tch, handle) = fillTemplate("unittest/translationtest",DOMTracker(),'gtk-x11-MozillaBrowser','platform')
  91.         compiled_templates.unittest.translationtest._ = self.oldgettext
  92.         text = tch.read()
  93.         text = HTMLPattern.match(text).group(1)
  94.         self.assertEqual(text,self.text)
  95.  
  96. class ReplaceTest(DemocracyTestCase):
  97.     def setUp(self):
  98.         handle = file(resources.path("testdata/replace-result"),"r")
  99.         self.text = handle.read()
  100.         handle.close()
  101.         self.text = HTMLPattern.match(self.text).group(1)
  102.     def test(self):
  103.         (tch, handle) = fillTemplate("unittest/replace",DOMTracker(),'gtk-x11-MozillaBrowser','platform')
  104.         text = tch.read()
  105.         text = HTMLPattern.match(text).group(1)
  106.         self.assertEqual(text,self.text)
  107.  
  108. class HideTest(DemocracyTestCase):
  109.     def setUp(self):
  110.         DemocracyTestCase.setUp(self)
  111.         handle = file(resources.path("testdata/hide-result"),"r")
  112.         self.text = handle.read()
  113.         handle.close()
  114.         self.text = HTMLPattern.match(self.text).group(1)
  115.     def test(self):
  116.         (tch, handle) = fillTemplate("unittest/hide",DOMTracker(),'gtk-x11-MozillaBrowser','platform')
  117.         text = tch.read()
  118.         text = HTMLPattern.match(text).group(1)
  119.         self.assertEqual(text,self.text)
  120.  
  121. class ViewTest(DemocracyTestCase):
  122.     pattern = re.compile("^\n<h1>view test template</h1>\n<span id=\"([^\"]+)\"/>\n", re.S)
  123.     containerPattern = re.compile("^\n<h1>view test template</h1>\n<div id=\"([^\"]+)\"></div>\n", re.S)
  124.     doublePattern = re.compile("^\n<h1>view test template</h1>\n<span id=\"([^\"]+)\"/>\n<span id=\"([^\"]+)\"/>\n", re.S)
  125.     updatePattern = re.compile("^\n<h1>update test template</h1>\n<span id=\"([^\"]+)\"/>\n", re.S)
  126.     hidePattern = re.compile("^\n<h1>update hide test template</h1>\n<div class=\"foo\" id=\"([^\"]+)\"", re.S)
  127.     itemPattern = re.compile("<div id=\"(.*?)\">\n<span>testview\d*</span>\n<span><span>object</span></span>\n<span><span>object</span></span>\n\n<div>\nhideIf:False\n<span>This is an include</span>\n\n<span>This is a template include</span>\n\n<span><span>This is a database replace</span></span>\n<span><span>This is a database replace</span></span>\n</div>\n</div>",re.S)
  128.  
  129.     def setUp(self):
  130.         global ranOnUnload
  131.         ranOnUnload = 0
  132.         DemocracyTestCase.setUp(self)
  133.         self.everything = database.defaultDatabase
  134.         self.x = HTMLObject('<span>object</span>')
  135.         self.y = HTMLObject('<span>object</span>')
  136.         self.domHandle = DOMTracker()
  137.  
  138.     def test(self):
  139.         (tch, handle) = fillTemplate("unittest/view",self.domHandle,'gtk-x11-MozillaBrowser','platform')
  140.         text = tch.read()
  141.         text = HTMLPattern.match(text).group(1)
  142.         self.assert_(self.pattern.match(text)) #span for template inserted
  143.         id = self.pattern.match(text).group(1)
  144.         handle.initialFillIn()
  145.         self.assertEqual(len(self.domHandle.callList),1)
  146.         self.assertEqual(self.domHandle.callList[0]['name'],'addItemBefore')
  147.         self.assertEqual(self.domHandle.callList[0]['id'],id)
  148.         match = self.itemPattern.findall(self.domHandle.callList[0]['xml'])
  149.         self.assertEqual(len(match),2)
  150.         self.assertNotEqual(match[0], match[1])
  151.         self.assertEqual(ranOnUnload, 0)
  152.         handle.unlinkTemplate()
  153.         self.assertEqual(ranOnUnload, 1)
  154.  
  155.     def testContainerDiv(self):
  156.         (tch, handle) = fillTemplate("unittest/view-container-div",self.domHandle,'gtk-x11-MozillaBrowser','platform')
  157.         text = tch.read()
  158.         text = HTMLPattern.match(text).group(1)
  159.         self.assert_(self.containerPattern.match(text)) #span for template inserted
  160.         id = self.containerPattern.match(text).group(1)
  161.         handle.initialFillIn()
  162.         self.assertEqual(len(self.domHandle.callList),1)
  163.         self.assertEqual(self.domHandle.callList[0]['name'],'addItemAtEnd')
  164.         self.assertEqual(self.domHandle.callList[0]['id'],id)
  165.         initialXML = self.domHandle.callList[0]['xml']
  166.         match = self.itemPattern.findall(initialXML)
  167.         self.assertEqual(len(match),2)
  168.         self.assertNotEqual(match[0], match[1])
  169.         handle.trackedViews[0].onResort()
  170.         self.assertEqual(len(self.domHandle.callList),3)
  171.         self.assertEqual(self.domHandle.callList[1]['name'],'changeItem')
  172.         self.assertEqual(self.domHandle.callList[1]['id'],id)
  173.         self.assertEqual(self.domHandle.callList[1]['xml'],
  174.                 '<div id="%s"></div>' % id)
  175.         self.assertEqual(self.domHandle.callList[2]['name'],'addItemAtEnd')
  176.         self.assertEqual(self.domHandle.callList[2]['id'],id)
  177.         self.assertEqual(self.domHandle.callList[2]['xml'], initialXML)
  178.  
  179.     def testUpdate(self):
  180.         (tch, handle) = fillTemplate("unittest/update",self.domHandle,'gtk-x11-MozillaBrowser','platform')
  181.         text = tch.read()
  182.         text = HTMLPattern.match(text).group(1)
  183.         self.assert_(self.updatePattern.match(text)) #span for template inserted
  184.         id = self.updatePattern.match(text).group(1)
  185.         handle.initialFillIn()
  186.         self.assertEqual(len(self.domHandle.callList),1)
  187.         self.assertEqual(self.domHandle.callList[0]['name'],'addItemBefore')
  188.         self.assertEqual(self.domHandle.callList[0]['id'],id)
  189.         match = self.itemPattern.findall(self.domHandle.callList[0]['xml'])
  190.         self.assertEqual(len(match),1)
  191.  
  192.         # This should do nothing, since the HTML didn't change
  193.         self.x.signalChange()
  194.         handle.updateRegions[0].doChange()
  195.         self.assertEqual(len(self.domHandle.callList),1)
  196.         
  197.         # Now, we get a callback
  198.         self.x.html = '<span>changes object</span>'
  199.         self.x.signalChange()
  200.         handle.updateRegions[0].doChange()
  201.         self.assertEqual(len(self.domHandle.callList),2)
  202.  
  203.         self.assertEqual(self.domHandle.callList[1]['name'],'changeItems')
  204.         self.assertEqual(self.domHandle.callList[1]['pairs'][0][0],match[0])
  205.         temp = HTMLObject('<span>object</span>')
  206.         handle.updateRegions[0].doChange()
  207.         self.assertEqual(len(self.domHandle.callList),3)
  208.         self.assertEqual(self.domHandle.callList[2]['name'],'changeItems')
  209.         self.assertEqual(self.domHandle.callList[2]['pairs'][0][0],match[0])
  210.         temp.remove()
  211.         handle.updateRegions[0].doChange()
  212.         self.assertEqual(len(self.domHandle.callList),4)
  213.         self.assertEqual(self.domHandle.callList[3]['name'],'changeItems')
  214.         self.assertEqual(self.domHandle.callList[3]['pairs'][0][0],match[0])
  215.         self.assertEqual(ranOnUnload, 0)
  216.         handle.unlinkTemplate()
  217.         self.assertEqual(ranOnUnload, 1)
  218.  
  219.     def testHide(self):
  220.         (tch, handle) = fillTemplate("unittest/update-hide",self.domHandle,'gtk-x11-MozillaBrowser','platform')
  221.         text = tch.read()
  222.         text = HTMLPattern.match(text).group(1)
  223.         self.assert_(self.hidePattern.match(text)) #span for template inserted
  224.         id = self.hidePattern.match(text).group(1)
  225.         handle.initialFillIn()
  226.         self.assertEqual(len(self.domHandle.callList),0)
  227.         self.x.signalChange()
  228.         self.assertEqual(len(self.domHandle.callList),0)
  229.         temp = HTMLObject('<span>object</span>')
  230.         self.assertEqual(len(self.domHandle.callList),1)
  231.         self.assertEqual(self.domHandle.callList[0]['name'],'showItem')
  232.         self.assertEqual(self.domHandle.callList[0]['id'],id)
  233.         temp.remove()
  234.         self.assertEqual(len(self.domHandle.callList),2)
  235.         self.assertEqual(self.domHandle.callList[1]['name'],'hideItem')
  236.         self.assertEqual(self.domHandle.callList[1]['id'],id)
  237.         self.assertEqual(ranOnUnload, 0)
  238.         handle.unlinkTemplate()
  239.         self.assertEqual(ranOnUnload, 1)
  240.  
  241.     def testTwoViews(self):
  242.         (tch, handle) = fillTemplate("unittest/view-double",self.domHandle,'gtk-x11-MozillaBrowser','platform')
  243.         text = tch.read()
  244.         text = HTMLPattern.match(text).group(1)
  245.         assert(self.doublePattern.match(text)) #span for template inserted
  246.         id = self.doublePattern.match(text).group(1)
  247.         id2 = self.doublePattern.match(text).group(2)
  248.         handle.initialFillIn()
  249.         self.assertEqual(len(self.domHandle.callList),2)
  250.         self.assertEqual(self.domHandle.callList[0]['name'],'addItemBefore')
  251.         self.assertEqual(self.domHandle.callList[1]['name'],'addItemBefore')
  252.         self.assert_(self.domHandle.callList[0]['id'] != self.domHandle.callList[1]['id'])
  253.         self.assert_(self.domHandle.callList[0]['id'] in [id, id2])
  254.         self.assert_(self.domHandle.callList[1]['id'] in [id, id2])
  255.         items1 = self.itemPattern.findall(self.domHandle.callList[0]['xml'])
  256.         items2 = self.itemPattern.findall(self.domHandle.callList[1]['xml'])
  257.  
  258.         match = copy.copy(items1)
  259.         match.extend(items2)
  260.         self.assertEqual(len(match),4)
  261.  
  262.         # This does nothing to the templates, since the HTML doesn't change
  263.         self.x.signalChange()
  264.         handle.trackedViews[0].callback()
  265.         handle.trackedViews[1].callback()
  266.         self.assertEqual(len(self.domHandle.callList),2)
  267.  
  268.         # Now, those calls are made
  269.         self.x.html = '<span>changed object</span>'
  270.         self.x.signalChange()
  271.         handle.trackedViews[0].callback()
  272.         handle.trackedViews[1].callback()
  273.         self.assertEqual(len(self.domHandle.callList),4)
  274.         
  275.         self.x.remove()
  276.         handle.trackedViews[0].callback()
  277.         handle.trackedViews[1].callback()
  278.         self.assertEqual(len(self.domHandle.callList),6)
  279.         self.assertEqual(self.domHandle.callList[0]['name'],'addItemBefore')
  280.         self.assertEqual(self.domHandle.callList[1]['name'],'addItemBefore')
  281.         self.assertEqual(self.domHandle.callList[2]['name'],'changeItems')
  282.         self.assertEqual(self.domHandle.callList[3]['name'],'changeItems')
  283.         self.assertEqual(self.domHandle.callList[4]['name'],'removeItems')
  284.         self.assertEqual(self.domHandle.callList[5]['name'],'removeItems')
  285.         changed1 = [p[0] for p in self.domHandle.callList[2]['pairs']]
  286.         changed2 = [p[0] for p in self.domHandle.callList[3]['pairs']]
  287.         self.assertEqual(len(changed1), 1)
  288.         self.assertEqual(len(changed2), 1)
  289.         self.assert_((changed1[0] in items1 and changed2[0] in items2) or
  290.                 changed1[0] in items2 and changed2[0] in items1)
  291.         self.assertEqual(self.domHandle.callList[4]['name'],'removeItems')
  292.         self.assertEqual(self.domHandle.callList[5]['name'],'removeItems')
  293.         self.assertEquals(len(self.domHandle.callList[4]['ids']), 1)
  294.         self.assertEquals(len(self.domHandle.callList[5]['ids']), 1)
  295.         self.assert_(((self.domHandle.callList[4]['ids'][0] in items1) and
  296.                           (self.domHandle.callList[5]['ids'][0] in items2)) or
  297.                          ((self.domHandle.callList[4]['ids'][0] in items2) and
  298.                           (self.domHandle.callList[5]['ids'][0] in items1)))
  299.  
  300.         self.x = HTMLObject('<span>object</span>')
  301.         handle.trackedViews[0].callback()
  302.         handle.trackedViews[1].callback()
  303.         self.assertEqual(len(self.domHandle.callList),8)
  304.         self.assertEqual(self.domHandle.callList[6]['name'],'addItemBefore')
  305.         match.extend(self.itemPattern.findall(self.domHandle.callList[6]['xml']))
  306.         self.assertEqual(self.domHandle.callList[7]['name'],'addItemBefore')
  307.         match.extend(self.itemPattern.findall(self.domHandle.callList[7]['xml']))
  308.         self.assertEqual(len(match),6)
  309.         for x in range(len(match)):
  310.             for y in range(x+1,len(match)):
  311.                 self.assertNotEqual(match[x],match[y])
  312.         self.assertEqual(ranOnUnload, 0)
  313.         handle.unlinkTemplate()
  314.         self.assertEqual(ranOnUnload, 1)
  315.  
  316. class TemplatePerformance(DemocracyTestCase):
  317.     def setUp(self):
  318.         global ranOnUnload
  319.         ranOnUnload = 0
  320.         DemocracyTestCase.setUp(self)
  321.         self.everything = database.defaultDatabase
  322.         self.domHandle = DOMTracker()
  323.  
  324.     def timeIt(self, func, repeat):
  325.         start = time()
  326.         for x in xrange(repeat):
  327.             func()
  328.         totalTime = time() - start
  329.         return totalTime
  330.  
  331.     def testRender(self):
  332.         self.feeds = []
  333.         self.items = []
  334.         for x in range(50):
  335.             self.feeds.append(feed.Feed(u'http://www.getdemocracy.com/50'))
  336.             for y in range(50):
  337.                 self.items.append(item.Item(feedparser.FeedParserDict(
  338.                     {'title': u"%d-%d" % (x, y),
  339.                      'enclosures': [{'url': u'file://%d-%d.mpg' % (x, y)}]}),
  340.                                             feed_id = self.feeds[-1].id
  341.                                             ))
  342.         
  343.         time1 = self.timeIt(self.fillAndUnlink, 10)
  344.  
  345.         for x in range(50):
  346.             for y in range(450):
  347.                 self.items.append(item.Item(feedparser.FeedParserDict(
  348.                     {'title': u"%d-%d" % (x, y),
  349.                      'enclosures': [{'url': u'file://%d-%d.mpg' % (x, y)}]}),
  350.                                             feed_id = self.feeds[x].id
  351.                                             ))
  352.         time2 = self.timeIt(self.fillAndUnlink, 10)
  353.  
  354.         # print "Filling in a 500 item feed took roughly %.4f secs" % (time2/10.0)
  355.         # Check that filling in 500 items takes no more than roughly
  356.         # 10x filling in 50 items
  357.         self.assert_(time2/time1 < 11, 'Template filling does not scale linearly')
  358.  
  359.  
  360.     def fillAndUnlink(self):
  361.         (tch, handle) = fillTemplate("channel",self.domHandle,'gtk-x11-MozillaBrowser','platform', id=self.feeds[-1].getID())
  362.         tch.read()
  363.         handle.initialFillIn()
  364.         handle.unlinkTemplate()
  365.  
  366. class OptimizedAttributeChangeTest(DemocracyTestCase):
  367.     def setUp(self):
  368.         self.changer = templateoptimize.HTMLChangeOptimizer()
  369.  
  370.     def checkChange(self, id, newXML, attributesDiff, htmlChanged):
  371.         changes = self.changer.calcChanges('abc123', newXML)
  372.         self.assertEquals(len(changes), 1)
  373.         self.assertEquals(changes[0][0], id)
  374.         self.assertEquals(changes[0][1], newXML)
  375.         self.assertEquals(changes[0][2].changedAttributes, attributesDiff)
  376.         if htmlChanged:
  377.             self.assert_(changes[0][2].changedInnerHTML is not None)
  378.         else:
  379.             self.assert_(changes[0][2].changedInnerHTML is None)
  380.  
  381.     def testBigChange(self):
  382.         first = '<div class="item" id="abc123">foo</div>'
  383.         second = '<div class="item" id="abc123">bar</div>'
  384.         self.changer.setInitialHTML('abc123', first)
  385.         self.checkChange('abc123', second, {}, True)
  386.  
  387.     def testNoChange(self):
  388.         first = '<div class="item" id="abc123">foo</div>'
  389.         self.changer.setInitialHTML('abc123', first)
  390.         changes = self.changer.calcChanges('abc123', first)
  391.         self.assertEquals(len(changes), 0)
  392.  
  393.     def testAttributeChange(self):
  394.         first = '<div class="item" id="abc123">foo</div>'
  395.         second = '<div class="item highlighed" id="abc123">foo</div>'
  396.         self.changer.setInitialHTML('abc123', first)
  397.         self.checkChange('abc123', second, {'class': 'item highlighed'},
  398.                 False)
  399.  
  400.     def testMultipleChanges(self):
  401.         first = '<div class="item" id="abc123">foo</div>'
  402.         second = '<div class="item highlighed" id="abc123">foo</div>'
  403.         third = '<div class="item highlighed" id="abc123">bar</div>'
  404.         fourth = '<div class="item" id="abc123">bar</div>'
  405.         self.changer.setInitialHTML('abc123', first)
  406.         self.checkChange('abc123', second, {'class': 'item highlighed'},
  407.                 False)
  408.         self.checkChange('abc123', third, {}, True)
  409.         self.checkChange('abc123', fourth, {'class': 'item'}, False)
  410.  
  411.     def testChangeWithoutInitalHTML(self):
  412.         first = '<div class="item" id="abc123">foo</div>'
  413.         self.assertRaises(KeyError, self.changer.calcChanges, 'abc123', first)
  414.  
  415. class HotspotOptimizedTest(DemocracyTestCase):
  416.     def setUp(self):
  417.         self.changer = templateoptimize.HTMLChangeOptimizer()
  418.  
  419.     def makeHotspotArea(self, outertext, innertext):
  420.         return """\
  421. <div id="outer">
  422.    <p>%s</p>
  423.    <!-- HOT SPOT inner --><span id="inner">%s</span><!-- HOT SPOT END -->
  424. </div>""" % (outertext, innertext)
  425.  
  426.     def makeMultiHotspotArea(self, outertext, innertext, innertext2):
  427.         return """\
  428. <div id="outer">
  429.    <p>%s</p>
  430.    <!-- HOT SPOT inner --><span id="inner">%s</span><!-- HOT SPOT END -->
  431.    <!-- HOT SPOT inner-2 --><span id="inner-2">%s</span><!-- HOT SPOT END -->
  432. </div>""" % (outertext, innertext, innertext2)
  433.  
  434.     def checkChange(self, newXML, *shouldChangeIDs):
  435.         changes = self.changer.calcChanges('outer', newXML)
  436.         actuallyChanged = [c[0] for c in changes]
  437.         self.assertEquals(set(shouldChangeIDs), set(actuallyChanged))
  438.  
  439.     def testHotspotChange(self):
  440.         first = self.makeHotspotArea('booya', 'booyaka')
  441.         second = self.makeHotspotArea('booya', 'booyaka booyaka')
  442.         self.changer.setInitialHTML('outer', first)
  443.         self.checkChange(second, 'inner')
  444.  
  445.     def testOutsideHotspotChange(self):
  446.         first = self.makeHotspotArea('foo', 'booyaka')
  447.         second = self.makeHotspotArea('bar', 'booyaka booyaka')
  448.         self.changer.setInitialHTML('outer', first)
  449.         self.checkChange(second, 'outer')
  450.  
  451.     def testMultipleHotspots(self):
  452.         first = self.makeMultiHotspotArea('foo', 'apples', 'bananas')
  453.         second = self.makeMultiHotspotArea('foo', 'apples', 'pears')
  454.         third = self.makeMultiHotspotArea('foo', 'kiwi', 'starfruit')
  455.         fourth = self.makeMultiHotspotArea('bar', 'kiwi', 'starfruit')
  456.         self.changer.setInitialHTML('outer', first)
  457.         self.checkChange(second, 'inner-2')
  458.         self.checkChange(third, 'inner', 'inner-2')
  459.         self.checkChange(fourth, 'outer')
  460.